package gitbucket.core.controller

import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date
import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service._
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.WebHookContentType
import gitbucket.core.model.activity.RenameRepositoryInfo
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId

import scala.util.Using
import org.scalatra.{Forbidden, Ok}

class RepositorySettingsController
    extends RepositorySettingsControllerBase
    with RepositoryService
    with AccountService
    with WebHookService
    with ProtectedBranchService
    with CommitStatusService
    with DeployKeyService
    with CustomFieldsService
    with ActivityService
    with OwnerAuthenticator
    with UsersAuthenticator
    with RequestCache

trait RepositorySettingsControllerBase extends ControllerBase {
    self: RepositoryService
        with AccountService
        with WebHookService
        with ProtectedBranchService
        with CommitStatusService
        with DeployKeyService
        with CustomFieldsService
        with ActivityService
        with OwnerAuthenticator
        with UsersAuthenticator =>

    // for repository options
    case class OptionsForm(
        description: Option[String],
        isPrivate: Boolean,
        issuesOption: String,
        externalIssuesUrl: Option[String],
        wikiOption: String,
        externalWikiUrl: Option[String],
        allowFork: Boolean,
        mergeOptions: Seq[String],
        defaultMergeOption: String,
        safeMode: Boolean
    )

    val optionsForm = mapping(
      "description" -> trim(label("Description", optional(text()))),
      "isPrivate" -> trim(label("Repository Type", boolean())),
      "issuesOption" -> trim(label("Issues Option", text(required, featureOption))),
      "externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
      "wikiOption" -> trim(label("Wiki Option", text(required, featureOption))),
      "externalWikiUrl" -> trim(label("External Wiki URL", optional(text(maxlength(200))))),
      "allowFork" -> trim(label("Allow Forking", boolean())),
      "mergeOptions" -> mergeOptions,
      "defaultMergeOption" -> trim(label("Default merge strategy", text(required))),
      "safeMode" -> trim(label("XSS protection", boolean()))
    )(OptionsForm.apply).verifying { form =>
        if (!form.mergeOptions.contains(form.defaultMergeOption)) {
            Seq("defaultMergeOption" -> s"This merge strategy isn't enabled.")
        } else Nil
    }

    // for default branch
    case class DefaultBranchForm(defaultBranch: String)

    val defaultBranchForm = mapping(
      "defaultBranch" -> trim(label("Default Branch", text(required, maxlength(100))))
    )(DefaultBranchForm.apply)

    // for deploy key
    case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean)

    val deployKeyForm = mapping(
      "title" -> trim(label("Title", text(required, maxlength(100)))),
      "publicKey" ->
          trim2(label("Key", text(required))), // TODO duplication check in the repository?
      "allowWrite" -> trim(label("Key", boolean()))
    )(DeployKeyForm.apply)

    // for web hook url addition
    case class WebHookForm(
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )

    def webHookForm(update: Boolean) = mapping(
      "url" -> trim(label("url", text(required, webHook(update)))),
      "events" -> webhookEvents,
      "ctype" -> label("ctype", text()),
      "token" -> optional(trim(label("token", text(maxlength(100)))))
    )((url, events, ctype, token) =>
        WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
    )

    // for rename repository
    case class RenameRepositoryForm(repositoryName: String)

    val renameForm = mapping(
      "repositoryName" -> trim(label(
        "New repository name",
        text(required, maxlength(100), repository, renameRepositoryName)
      ))
    )(RenameRepositoryForm.apply)

    // for transfer ownership
    case class TransferOwnerShipForm(newOwner: String)

    val transferForm = mapping(
      "newOwner" -> trim(label("New owner", text(required, transferUser)))
    )(TransferOwnerShipForm.apply)

    // for custom field
    case class CustomFieldForm(
        fieldName: String,
        fieldType: String,
        constraints: Option[String],
        enableForIssues: Boolean,
        enableForPullRequests: Boolean
    )

    val customFieldForm = mapping(
      "fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
      "fieldType" -> trim(label("Field type", text(required))),
      "constraints" -> trim(label("Constraints", optional(text()))),
      "enableForIssues" -> trim(label("Enable for issues", boolean(required))),
      "enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required)))
    )(CustomFieldForm.apply)

    /**
   * Redirect to the Options page.
   */
    get("/:owner/:repository/settings")(ownerOnly { repository =>
        redirect(s"/${repository.owner}/${repository.name}/settings/options")
    })

    /**
   * Display the Options page.
   */
    get("/:owner/:repository/settings/options")(ownerOnly { html.options(_, flash.get("info")) })

    /**
   * Save the repository options.
   */
    post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
        saveRepositoryOptions(
          repository.owner,
          repository.name,
          form.description,
          repository.repository.parentUserName.map { _ =>
              repository.repository.isPrivate
          } getOrElse form.isPrivate,
          form.issuesOption,
          form.externalIssuesUrl,
          form.wikiOption,
          form.externalWikiUrl,
          form.allowFork,
          form.mergeOptions,
          form.defaultMergeOption,
          form.safeMode
        )
        flash.update("info", "Repository settings has been updated.")
        redirect(s"/${repository.owner}/${repository.name}/settings/options")
    })

    /** branch settings */
    get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
        val protecteions = getProtectedBranchList(repository.owner, repository.name)
        html.branches(repository, protecteions, flash.get("info"))
    })

    /** Update default branch */
    post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(
      ownerOnly { (form, repository) =>
          if (!repository.branchList.contains(form.defaultBranch)) {
              redirect(s"/${repository.owner}/${repository.name}/settings/options")
          } else {
              saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
              // Change repository HEAD
              Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
                  git.getRepository.updateRef(Constants.HEAD, true)
                      .link(Constants.R_HEADS + form.defaultBranch)
              }
              flash.update("info", "Repository default branch has been updated.")
              redirect(s"/${repository.owner}/${repository.name}/settings/branches")
          }
      }
    )

    /** Branch protection for branch */
    get("/:owner/:repository/settings/branches/*")(ownerOnly { repository =>
        import gitbucket.core.api._
        val branch = params("splat")

        if (!repository.branchList.contains(branch)) {
            redirect(s"/${repository.owner}/${repository.name}/settings/branches")
        } else {
            val protection = ApiBranchProtection(
              getProtectedBranchInfo(repository.owner, repository.name, branch)
            )
            val lastWeeks = getRecentStatusContexts(
              repository.owner,
              repository.name,
              Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC))
            ).toSet
            val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
            html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
        }
    })

    /**
   * Display the Collaborators page.
   */
    get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
        html.collaborators(
          getCollaborators(repository.owner, repository.name),
          getAccountByUserName(repository.owner).get.isGroupAccount,
          repository
        )
    })

    post("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
        val collaborators = params("collaborators")
        removeCollaborators(repository.owner, repository.name)
        collaborators.split(",").withFilter(_.nonEmpty).foreach { collaborator =>
            val userName :: role :: Nil = collaborator.split(":").toList
            addCollaborator(repository.owner, repository.name, userName, role)
        }
        redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
    })

    /**
   * Display the web hook page.
   */
    get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
        html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
    })

    /**
   * Display the web hook edit page.
   */
    get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
        val webhook = RepositoryWebHook(
          userName = repository.owner,
          repositoryName = repository.name,
          url = "",
          ctype = WebHookContentType.FORM,
          token = None
        )
        html.edithook(webhook, Set(WebHook.Push), repository, true)
    })

    /**
   * Add the web hook URL.
   */
    post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly {
        (form, repository) =>
            addWebHook(
              repository.owner,
              repository.name,
              form.url,
              form.events,
              form.ctype,
              form.token
            )
            flash.update("info", s"Webhook ${form.url} created")
            redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
    })

    /**
   * Delete the web hook URL.
   */
    get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
        deleteWebHook(repository.owner, repository.name, params("url"))
        flash.update("info", s"Webhook ${params("url")} deleted")
        redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
    })

    /**
   * Send the test request to registered web hook URLs.
   */
    ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
        def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h =>
            Array(h.getName, h.getValue)
        }

        Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
            import scala.concurrent.duration._
            import scala.concurrent._
            import scala.jdk.CollectionConverters._
            import scala.util.control.NonFatal
            import org.apache.http.util.EntityUtils
            import scala.concurrent.ExecutionContext.Implicits.global

            val url = params("url")
            val token = Some(params("token"))
            val ctype = WebHookContentType.valueOf(params("ctype"))
            val dummyWebHookInfo = RepositoryWebHook(
              userName = repository.owner,
              repositoryName = repository.name,
              url = url,
              ctype = ctype,
              token = token
            )
            val dummyPayload = {
                val ownerAccount = getAccountByUserName(repository.owner).get
                val commits =
                    if (JGitUtil.isEmpty(git)) List.empty
                    else git.log.add(git.getRepository.resolve(repository.repository.defaultBranch))
                        .setMaxCount(4).call.iterator.asScala.map(new CommitInfo(_)).toList
                val pushedCommit = commits.drop(1)

                WebHookPushPayload(
                  git = git,
                  sender = ownerAccount,
                  refName = "refs/heads/" + repository.repository.defaultBranch,
                  repositoryInfo = repository,
                  commits = pushedCommit,
                  repositoryOwner = ownerAccount,
                  oldId = commits.lastOption.map(_.id).map(ObjectId.fromString)
                      .getOrElse(ObjectId.zeroId()),
                  newId = commits.headOption.map(_.id).map(ObjectId.fromString)
                      .getOrElse(ObjectId.zeroId())
                )
            }

            val (webHook, json, reqFuture, resFuture) =
                callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings)
                    .head

            val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
                case e: java.net.UnknownHostException =>
                    Map("error" -> ("Unknown host " + e.getMessage))
                case e: java.lang.IllegalArgumentException => Map("error" -> ("invalid url"))
                case e: org.apache.http.client.ClientProtocolException =>
                    Map("error" -> ("invalid url"))
                case NonFatal(e) => Map("error" -> (s"${e.getClass} ${e.getMessage}"))
            }

            contentType = formats("json")
            org.json4s.jackson.Serialization.write(Map(
              "url" -> url,
              "request" -> Await.result(
                reqFuture
                    .map(req => Map("headers" -> _headers(req.getAllHeaders), "payload" -> json))
                    .recover(toErrorMap),
                20 seconds
              ),
              "response" -> Await.result(
                resFuture.map(res =>
                    Map(
                      "status" -> res.getStatusLine.getStatusCode,
                      "body" -> EntityUtils.toString(res.getEntity()),
                      "headers" -> _headers(res.getAllHeaders())
                    )
                ).recover(toErrorMap),
                20 seconds
              )
            ))
        }
    })

    /**
   * Display the web hook edit page.
   */
    get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
        getWebHook(repository.owner, repository.name, params("url")).map { case (webhook, events) =>
            html.edithook(webhook, events, repository, false)
        } getOrElse NotFound()
    })

    /**
   * Update web hook settings.
   */
    post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly {
        (form, repository) =>
            updateWebHook(
              repository.owner,
              repository.name,
              form.url,
              form.events,
              form.ctype,
              form.token
            )
            flash.update("info", s"webhook ${form.url} updated")
            redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
    })

    /**
   * Display the danger zone.
   */
    get("/:owner/:repository/settings/danger")(ownerOnly { html.danger(_, flash.get("info")) })

    /**
   * Rename repository.
   */
    post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
        context.withLoginAccount { loginAccount =>
            if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
                if (repository.name != form.repositoryName) {
                    // Update database and move git repository
                    renameRepository(
                      repository.owner,
                      repository.name,
                      repository.owner,
                      form.repositoryName
                    )
                    // Record activity log
                    val renameInfo = RenameRepositoryInfo(
                      repository.owner,
                      form.repositoryName,
                      loginAccount.userName,
                      repository.name
                    )
                    recordActivity(renameInfo)
                }
                redirect(s"/${repository.owner}/${form.repositoryName}")
            } else Forbidden()
        }
    })

    /**
   * Transfer repository ownership.
   */
    post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
        context.withLoginAccount { loginAccount =>
            if (
              context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin
            ) {
                // Change repository owner
                if (repository.owner != form.newOwner) {
                    // Update database and move git repository
                    renameRepository(
                      repository.owner,
                      repository.name,
                      form.newOwner,
                      repository.name
                    )
                    // Record activity log
                    val renameInfo = RenameRepositoryInfo(
                      form.newOwner,
                      repository.name,
                      loginAccount.userName,
                      repository.owner
                    )
                    recordActivity(renameInfo)
                }
                redirect(s"/${form.newOwner}/${repository.name}")
            } else Forbidden()
        }
    })

    /**
   * Delete the repository.
   */
    post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
        context.withLoginAccount { loginAccount =>
            if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) {
                // Delete the repository and related files
                deleteRepository(repository.repository)
                redirect(s"/${repository.owner}")
            } else Forbidden()
        }
    })

    /**
   * Run GC
   */
    post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
        LockUtil.lock(s"${repository.owner}/${repository.name}") {
            Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
                git.gc().call()
            }
        }
        flash.update("info", "Garbage collection has been executed.")
        redirect(s"/${repository.owner}/${repository.name}/settings/danger")
    })

    /** List deploy keys */
    get("/:owner/:repository/settings/deploykey")(ownerOnly { repository =>
        html.deploykey(repository, getDeployKeys(repository.owner, repository.name))
    })

    /** Register a deploy key */
    post("/:owner/:repository/settings/deploykey", deployKeyForm)(ownerOnly { (form, repository) =>
        addDeployKey(repository.owner, repository.name, form.title, form.publicKey, form.allowWrite)
        redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
    })

    /** Delete a deploy key */
    get("/:owner/:repository/settings/deploykey/delete/:id")(ownerOnly { repository =>
        val deployKeyId = params("id").toInt
        deleteDeployKey(repository.owner, repository.name, deployKeyId)
        redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
    })

    /** Custom fields for issues and pull requests */
    get("/:owner/:repository/settings/issues")(ownerOnly { repository =>
        val customFields = getCustomFields(repository.owner, repository.name)
        html.issues(customFields, repository)
    })

    /** New custom field form */
    get("/:owner/:repository/settings/issues/fields/new")(ownerOnly { repository =>
        html.issuesfieldform(None, repository)
    })

    /** Add custom field */
    ajaxPost("/:owner/:repository/settings/issues/fields/new", customFieldForm)(ownerOnly {
        (form, repository) =>
            val fieldId = createCustomField(
              repository.owner,
              repository.name,
              form.fieldName,
              form.fieldType,
              if (form.fieldType == "enum") form.constraints else None,
              form.enableForIssues,
              form.enableForPullRequests
            )
            html.issuesfield(getCustomField(repository.owner, repository.name, fieldId).get)
    })

    /** Edit custom field form */
    ajaxGet("/:owner/:repository/settings/issues/fields/:fieldId/edit")(ownerOnly { repository =>
        getCustomField(repository.owner, repository.name, params("fieldId").toInt).map {
            customField => html.issuesfieldform(Some(customField), repository)
        } getOrElse NotFound()
    })

    /** Update custom field */
    ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/edit", customFieldForm)(
      ownerOnly { (form, repository) =>
          updateCustomField(
            repository.owner,
            repository.name,
            params("fieldId").toInt,
            form.fieldName,
            form.fieldType,
            if (form.fieldType == "enum") form.constraints else None,
            form.enableForIssues,
            form.enableForPullRequests
          )
          html.issuesfield(
            getCustomField(repository.owner, repository.name, params("fieldId").toInt).get
          )
      }
    )

    /** Delete custom field */
    ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/delete")(ownerOnly { repository =>
        deleteCustomField(repository.owner, repository.name, params("fieldId").toInt)
        Ok()
    })

    /**
   * Provides duplication check for web hook url.
   */
    private def webHook(needExists: Boolean): Constraint = new Constraint() {
        override def validate(name: String, value: String, messages: Messages): Option[String] =
            if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
                Some(if (needExists) { "URL had not been registered yet." }
                else { "URL had been registered already." })
            } else { None }
    }

    private def webhookEvents = new ValueType[Set[WebHook.Event]] {
        def convert(
            name: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Set[WebHook.Event] = {
            WebHook.Event.values.flatMap { t => params.get(name + "." + t.name).map(_ => t) }.toSet
        }
        def validate(
            name: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Seq[(String, String)] =
            if (convert(name, params, messages).isEmpty) {
                Seq(name -> messages("error.required").format(name))
            } else { Nil }
    }

//  /**
//   * Provides Constraint to validate the collaborator name.
//   */
//  private def collaborator: Constraint = new Constraint(){
//    override def validate(name: String, value: String, messages: Messages): Option[String] =
//      getAccountByUserName(value) match {
//        case None => Some("User does not exist.")
////        case Some(x) if(x.isGroupAccount)
////                  => Some("User does not exist.")
//        case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
//                  => Some(value + " is repository owner.") // TODO also group members?
//        case _    => None
//      }
//  }

    /**
   * Duplicate check for the rename repository name.
   */
    private def renameRepositoryName: Constraint = new Constraint() {
        override def validate(
            name: String,
            value: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Option[String] = {
            for {
                repoName <- params.optionValue("repository") if repoName != value
                userName <- params.optionValue("owner")
                _ <- getRepositoryNamesOfUser(userName).find(_ == value)
            } yield { "Repository already exists." }
        }
    }

    /**
   */
    private def featureOption: Constraint = new Constraint() {
        override def validate(
            name: String,
            value: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Option[String] =
            if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None
            else Some("Option is invalid.")
    }

    /**
   * Provides Constraint to validate the repository transfer user.
   */
    private def transferUser: Constraint = new Constraint() {
        override def validate(name: String, value: String, messages: Messages): Option[String] =
            getAccountByUserName(value) match {
                case None => Some("User does not exist.")
                case Some(x) =>
                    if (x.userName == params("owner")) { Some("This is current repository owner.") }
                    else {
                        params.get("repository").flatMap { repositoryName =>
                            getRepositoryNamesOfUser(x.userName).find(_ == repositoryName)
                                .map { _ => "User already has same repository." }
                        }
                    }
            }
    }

    private def mergeOptions = new ValueType[Seq[String]] {
        override def convert(
            name: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Seq[String] = { params.getOrElse("mergeOptions", Nil) }
        override def validate(
            name: String,
            params: Map[String, Seq[String]],
            messages: Messages
        ): Seq[(String, String)] = {
            val mergeOptions = params.getOrElse("mergeOptions", Nil)
            if (mergeOptions.isEmpty) {
                Seq("mergeOptions" -> "At least one option must be enabled.")
            } else if (
              !mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))
            ) { Seq("mergeOptions" -> "mergeOptions are invalid.") }
            else { Nil }
        }
    }

}
